/**
 * Copyright (C) 2011 Vinicius Vielmo Cogo, Marcelo Pasin, and the authors
 * indicated in the @author tags.
 * 
 * This file is part of DiversityAgent.
 * 
 * DiversityAgent is a free software: you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published by the
 * Free Software Foundation, either version 3 of the License, or (at your
 * option) any later version.
 * 
 * DiversityAgent is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
 * details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with DiversityAgent. If not, see <http://www.gnu.org/licenses/>.
 */
package drivers;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.Properties;

import org.opennebula.client.Client;
import org.opennebula.client.OneResponse;
import org.opennebula.client.host.Host;
import org.opennebula.client.host.HostPool;
import org.opennebula.client.vm.VirtualMachinePool;

import diversity_agent.DataContainer;
import diversity_agent.VirtualMachine;

/**
 * 
 * @author Vinicius Vielmo Cogo (vielmo at lasige.di.fc.ul.pt)
 * @version 0.1-beta
 * @since 2011-11-18
 * 
 */
public class OpenNebula extends diversity_agent.Cloud
{

	private Client	mOneClient;

	/**
	 * 
	 * @param theCloudName
	 * @param theTool
	 * @param theType
	 * @param theAddress
	 * @param theUsername
	 * @param thePassword
	 */
	public OpenNebula(String theCloudName, String theTool, String theType,
			String theAddress, String theUsername, String thePassword)
	{
		super(theCloudName, theTool, theType, theAddress);
		try {
			this.mOneClient = new Client((theUsername + ":" + thePassword)
					.toString(), ("http://" + theAddress + ":2633/RPC2")
					.toString());
			if (this.mOneClient == null) {
				throw new NullPointerException("Failed to connect!");
			}
		} catch (Exception e) {
			throw new NullPointerException("Failed to connect!");
		}
	}

	/**
	 * 
	 */
	public VirtualMachine createVm(Properties theProperties,
			DataContainer theDataContainer)
	{
		try {
			int aIdOnAgent = Integer.parseInt(theProperties
					.getProperty("vm.id.agent"));
			// Create a new VirtualMachine instance
			VirtualMachine aVirtualMachine = new VirtualMachine(aIdOnAgent);
			// Set the cloud provider
			aVirtualMachine.setCloudName(this.getCloudName());
			// Initialise the VM template
			String aVmTemplate = "NAME = ";
			if (theProperties.getProperty("vm.name.prefix") != null) {
				aVmTemplate += theProperties.getProperty("vm.name.prefix")
						+ "-";
			}
			aVmTemplate += aIdOnAgent;
			if (theProperties.getProperty("vm.name.suffix") != null) {
				aVmTemplate += "-"
						+ theProperties.getProperty("vm.name.suffix");
			}
			aVmTemplate += "\n";
			// Get physical CPU property
			if (theProperties.getProperty("vm.cpu") != null) {
				aVmTemplate += "CPU = " + theProperties.getProperty("vm.cpu")
						+ "\n";
			} else {
				aVmTemplate += "CPU = 1\n";
			}
			// Get virtual CPU property
			if (theProperties.getProperty("vm.vcpu") != null) {
				aVmTemplate += "VCPU = " + theProperties.getProperty("vm.vcpu")
						+ "\n";
			} else {
				aVmTemplate += "VCPU = 1\n";
			}
			// Get memory property
			if (theProperties.getProperty("vm.memory") != null) {
				aVmTemplate += "MEMORY = "
						+ theProperties.getProperty("vm.memory") + "\n";
			} else {
				aVmTemplate += "MEMORY = 1024\n";
			}
			// Get network property
			if (theProperties.getProperty("vm.network") != null) {
				aVmTemplate += "NIC = [ NETWORK = \""
						+ theProperties.getProperty("vm.network") + "\" ]\n";
			} else {
				aVmTemplate += "NIC = [ NETWORK = \"Public\" ]\n";
			}
			// Set the VM image (disk.image.name is not a mandatory property)
			String aImageName = theProperties.getProperty("disk.image.name");
			if (aImageName == null) {
				aImageName = theDataContainer.retrieveAnyImage(
						this.getCloudName()).getName();
				if (aImageName == null) {
					return null;
				}
			}
			aVirtualMachine.setImageName(aImageName);
			aVmTemplate += "DISK = [ image = \"" + aImageName + "\" ]\n";
			// Prepare other important properties
			String aRawProperties = "OS = [ kernel = /usr/lib/xen-default/boot/hvmloader ]\n"
					+ "RAW = [ TYPE = \"xen\", \n"
					+ "DATA = \"builder = 'hvm' \n"
					+ "shadow_memory = 8\n"
					+ "device_model = '/usr/lib/xen-default/bin/qemu-dm'\n"
					+ "boot = \\\"c\\\"\"]";
			aVmTemplate += aRawProperties;
			// Prepare the rank and requirements regarding host name
			if (theProperties.getProperty("host.name.differ") != null) {
				String[] aUsedHosts = theProperties.getProperty(
						"host.name.differ").split(",");
				String aHostsRestriction = "";
				ArrayList<String> aAvailableHosts = getAvailableHosts();
				if (aAvailableHosts.size() > aUsedHosts.length) {
					for (int i = 0 ; i < aUsedHosts.length ; i++) {
						if (aUsedHosts.length > 0) {
							aHostsRestriction += "\"HOSTNAME != \\\""
									+ aUsedHosts[i] + "\\\"\"";
							if (i < (aUsedHosts.length - 1)) {
								aHostsRestriction += " & ";
							}
						}
					}
					if (!aHostsRestriction.equals("")) {
						aVmTemplate += "\nREQUIREMENTS = " + aHostsRestriction;
					}
				} else {
					aVmTemplate += "\nRANK = \"- RUNNING_VMS\"";
				}
			} else if (theProperties.getProperty("host.name.equal") != null) {
				aVmTemplate += "\nREQUIREMENTS = \"HOSTNAME = \\\""
						+ theProperties.getProperty("host.name.equal")
						+ "\\\"\"";
			}
			// Ask the VM to the cloud provider
			OneResponse rc = org.opennebula.client.vm.VirtualMachine.allocate(
					mOneClient, aVmTemplate);
			if (rc.isError()) {
				System.out.println(rc.getErrorMessage());
				return null;
			}
			// Verify the ID on Cloud
			int aNewVmId = Integer.parseInt(rc.getMessage());
			if (aNewVmId == -1) {
				System.out.println("[ERROR     ] Problems on VM creation.");
				return null;
			} else {
				aVirtualMachine.setIdOnCloud(aNewVmId);
			}
			// Wait the machine becoming ready
			while (vmPending(aNewVmId)) {
			}
			;
			// Verify if VM failed
			if (getVmStatus(aNewVmId).equals("fail")) {
				this.incrementCreatedVms();
				return null;
			}
			// Verify the VM IP address
			String aIpAddress = readVm(aNewVmId, "template/nic/ip");
			if (aIpAddress.equals("")) {
				System.out
						.println("[ERROR     ] Problems to get the VM IP address.");
				return null;
			} else {
				aVirtualMachine.setIpAddress(aIpAddress);
			}
			// Verify the host name
			String aHostName = readVm(aNewVmId, "history/hostname");
			if (aHostName.equals("")) {
				System.out
						.println("[ERROR     ] Problems to get the hostname.");
				return null;
			} else {
				aVirtualMachine.setHostName(aHostName);
			}
			// Increment cloud counters
			this.incrementCreatedVms();
			this.incrementRunningVms();
			// Return the VM instance
			return aVirtualMachine;
		} catch (Exception e) {
			return null;
		}
	}

	/**
	 * 
	 */
	@Override
	public boolean updateVm()
	{
		// [TODO] This function can perform all other actions on VMs (e.g.
		// migrate)
		return false;
	}

	/**
	 * 
	 */
	@Override
	public boolean deleteVm(int theVmIdOnCloud)
	{
		try {
			org.opennebula.client.vm.VirtualMachine v = new org.opennebula.client.vm.VirtualMachine(
					theVmIdOnCloud, mOneClient);
			OneResponse rc = v.finalizeVM();
			if (rc.isError()) {
				System.out.println("failed!");
				throw new Exception(rc.getErrorMessage());
			}

			this.decrementRunningVms();
		} catch (Exception e) {
			System.out.println(e.getMessage());
		}
		return true;
	}

	/**
	 * 
	 */
	@Override
	public String getVmStatus(int theVmIdOnCloud)
	{
		VirtualMachinePool mVMP;
		mVMP = new VirtualMachinePool(mOneClient, -2);
		@SuppressWarnings("unused")
		OneResponse rc = mVMP.info();
		Iterator<org.opennebula.client.vm.VirtualMachine> aIterator = mVMP
				.iterator();
		org.opennebula.client.vm.VirtualMachine aVm;
		while (aIterator.hasNext()) {
			aVm = (org.opennebula.client.vm.VirtualMachine) aIterator.next();
			if (Integer.parseInt(aVm.getId()) == theVmIdOnCloud) {
				return aVm.status();
			}
		}
		return new String("");
	}

	/**
	 * 
	 */
	@Override
	public String readVm(int theVmId, String theProperty)
	{
		VirtualMachinePool mVMP;
		mVMP = new VirtualMachinePool(mOneClient, -2);
		@SuppressWarnings("unused")
		OneResponse rc = mVMP.info();
		Iterator<org.opennebula.client.vm.VirtualMachine> aIterator = mVMP
				.iterator();
		org.opennebula.client.vm.VirtualMachine aVm;
		while (aIterator.hasNext()) {
			aVm = (org.opennebula.client.vm.VirtualMachine) aIterator.next();
			if (Integer.parseInt(aVm.getId()) == theVmId) {
				return aVm.xpath(theProperty);
			}
		}
		return null;
	}

	/**
	 * 
	 * @return
	 */
	private ArrayList<String> getAvailableHosts()
	{
		HostPool mHP;
		ArrayList<String> aAvailableHosts = new ArrayList<String>();
		mHP = new HostPool(mOneClient);
		@SuppressWarnings("unused")
		OneResponse rc = mHP.info();
		Iterator<Host> aIterator = mHP.iterator();
		Host aHost;
		while (aIterator.hasNext()) {
			aHost = aIterator.next();
			aAvailableHosts.add(aHost.getName());
		}
		return aAvailableHosts;
	}

	/**
	 * 
	 * @param theVmIdOnCloud
	 * @return
	 */
	private boolean vmPending(int theVmIdOnCloud)
	{
		String theVmStatus = getVmStatus(theVmIdOnCloud);
		if ((theVmStatus != null)
				&& (theVmStatus.equals("runn") || theVmStatus.equals("fail"))) {
			return false;
		} else {
			return true;
		}
	}

}
